package cld_client;

use English;
use Socket;
use IO::Select;
use IO::Socket::INET;
use Digest::MD5 qw(md5_hex);
use Crypt::CBC;
use Crypt::Blowfish;
use Net::Ping;

use constant PF_PACKET => 17; # not defined as yet!
# use constant SOCK_PACKET => 10; 
$cld_client::last_error=undef;
$cld_client::last_rc=0;

#########################################################################
# The new method creates a new client object, and requires the		#
# following arguments:							#
# KEY	The key used to encode/decode packets.				#
# INET	The internet address to connect to.				#
# PORT	The port number to connect to.					#
# [ILIST] Complete list of internet addresses to use - comma		#
#	  separated. If specified 					#
#         each connection attempt will ping first and use what		#
#	  INET was, otherwise it will use an alternative for this	#
#	  connection - timeout is .25 seconds...			#
#	  If no responses returns undef when a message is sent.		#
#########################################################################

sub new {
my $proto=shift;
my %args=@_;
my $class=ref($proto) || $proto;
my $self={};

	if(!exists($args{KEY}) || !exists($args{PORT}) ||
		!exists($args{INET})) {
		$cld_client::last_error="Missing function arguments";
		$cld_client::last_rc=1;
		return undef;
	}

	if(exists($args{ILIST}) && defined($args{ILIST})) {
		my @L=split(/,/,$args{ILIST});
		$self->{ILIST}=[@L];
	}
	$self->{KEY}=$args{KEY};
	$self->{HOST}=$args{INET};
	$self->{INET}=$args{INET};
	$self->{PING}=Net::Ping->new("icmp");
	$self->{PORT}=$args{PORT};
	$self->{SOCKET}=undef;
	$self->{CRYPT}=Crypt::CBC->new($args{KEY},"Blowfish");
	bless($self,$class);
	return $self;
}

#########################################################################
# This method will connect to the server, send the message and return	#
# the response.								#
# The message to send is in the MSG argument.				#
# It can be called with the connection open , or closed.		#
# Takes optional SENDONLY argument.					#
#########################################################################

sub send_msg {
my $self=shift;
my %args=@_;
my $selector;
my ($md5,$encoded_msg,$fd,$old_fd);
my ($md52,$decoded_msg,$server_response,$plain_text);
my (@sockets,$sock);
my $timeout=5;
my $sendonly=0;

	$sendonly=$args{SENDONLY} if exists($args{SENDONLY});
	if(exists($args{TIMEOUT})) {
		$timeout=$args{TIMEOUT};
	}
	if(!exists($args{MSG})) {
		$cld_client::last_error="Missing function arguments";
		$cld_client::last_rc=1;
		return undef;
	}

	$md5=md5_hex($args{MSG});
	$encoded_msg=$self->{CRYPT}->encrypt_hex($args{MSG});

	#################################################################
	# If the socket is not open, then we now open it.		#
	#################################################################

	if(!defined($self->{SOCKET})) {
		#########################################################
		# If ILIST then always verify connection first.		#
		#########################################################

		if(exists($self->{ILIST})) {
			my $ok=1;
			my $sip=$self->{HOST};
			if(! $self->{PING}->ping($sip,0.25)) {
				$ok=0;
				my $start_delay=0.25;
				my $total_time=0;
				my $cip;
				while(1) {
					for $cip (@{$self->{ILIST}}) {
						if($self->{PING}->ping($cip,$start_delay)) {
							$self->{HOST}=$cip;
							$ok=1;
							last;
						}
						$total_time+=$start_delay;
					}
					last if $ok;
					if($total_time>$timeout) {
						last;
					}
					$start_delay+=0.25;
				}
			}
			if(! $ok) {
				$cld_client::last_errro="Unable to connect to server on any IP address!";
				$cld_client::last_rc=2;
				return undef;
			}
		}
		my ($iaddr,$paddr,$proto,$port);

		$port=$self->{PORT};
		if ($port =~ /\D/) { $port = getservbyname($port, 'tcp'); }
		die("no port") unless $port;
		$iaddr  = inet_aton($self->{HOST}) or die("no host: $!");
		$paddr	= sockaddr_in($port, $iaddr) or die("no paddr");
		$proto   = getprotobyname('tcp');
		socket($fd, PF_INET, SOCK_STREAM, $proto)  || die "socket: $!";
		my $rr=0;
		eval {
			local $SIG{ALRM} = sub { die "alarm clock restart" };
			alarm $timeout;
			if(!connect($fd, $paddr)) {
				alarm 0;
				$cld_client::last_errro="Unable to connect to server";
				$cld_client::last_rc=2;
				$rr=1;
				return;
			}
			alarm 0;
		};
		return(undef) if $rr;
		if ($@ and $@ =~ /alarm clock restart/) {
			$cld_client::last_errro="Unable to connect to server";
			$cld_client::last_rc=2;
			return undef;
		}

		$self->{SOCKET}=$fd;

		#########################################################
		# Set autoflush on the socket to ensure our message	#
		# is sent and thus responded to as we expect.		#
		#########################################################

		$old_fd=select($fd);
		$|=1;
		select($old_fd);
	} else {
		$fd=$self->{SOCKET};
	}

	#################################################################
	# Send the message, checking for any problems...		#
	#################################################################

	my $x;
	eval {
		local $SIG{ALRM} = sub { die "alarm clock restart" };
		alarm $timeout;
		$x=print $fd "$md5,$encoded_msg\n";
		alarm 0;
	};
	if ($@ and $@ =~ /alarm clock restart/) {
		$cld_client::last_errro="Timeout whilst writing message to server";
		$cld_client::last_rc=4;
		close($fd);
		$self->{SOCKET}=undef;
		$fd=undef;
		return undef;
	}

	#################################################################
	# Return a dummy OK response if we only want to send out the	#
	# message and not wait for a reply.				#
	#################################################################
	if($sendonly) {
		$self->{SOCKET}=undef;
		close($fd);
		return "OK";
	}
	if(!$x) {
		$cld_client::last_errro="Unable to write message to server";
		$cld_client::last_rc=3;
		close($fd);
		$self->{SOCKET}=undef;
		$fd=undef;
		return undef;
	}

	#################################################################
	# Now the message has been sent, we must await the response.	#
	#################################################################

	eval {
		local $SIG{ALRM} = sub { die "alarm clock restart" };
		alarm $timeout;
		$server_response=<$fd>;
		alarm 0;
	};

	if ($@ and $@ =~ /alarm clock restart/) {
		$cld_client::last_errro="Timeout whilst waiting for message from server";
		$cld_client::last_rc=4;
		close($fd);
		$self->{SOCKET}=undef;
		return(undef);
	}

	if(! defined($server_response)) {
		$cld_client::last_error="Broken socket before client request completed.";
		$cld_client::last_rc=4;
		close($fd);
		$self->{SOCKET}=undef;
		return undef;
	}
	chomp($server_response);
	($md5,$decoded_msg)=($server_response =~ /([A-Fa-f0-9]+),(.*)/);
	if(!defined($md5) || !defined($decoded_msg)) {
		$cld_client::last_error="Invalid message format.";
		$cld_client::last_rc=5;
		close($fd);
		$self->{SOCKET}=undef;
		return undef;
	}

	$plain_text=$self->{CRYPT}->decrypt_hex($decoded_msg);
	if(!defined($plain_text)) {
		$cld_client::last_error="Invalid message format.";
		$cld_client::last_rc=6;
		close($fd);
		$self->{SOCKET}=undef;
		return undef;
	}
	$md52=md5_hex($plain_text);
	if($md52 ne $md5) {
		$cld_client::last_error="Invalid encryption key used for message.";
		$cld_client::last_rc=7;
		close($fd);
		$self->{SOCKET}=undef;
		return undef;
	}
	close($fd);
	$self->{SOCKET}=undef;
	return $plain_text;
}

#########################################################################
# If the specified connection is open, thn we attempt to close it.	#
#########################################################################

sub close_session {
my $self=shift;

	if(defined($self->{SOCKET})) {
		# $self->{SOCKET}->close;
		close($self->{SOCKET});
		$self->{SOCKET}=undef;
	}
}

1;
